<?php
/**
 * This file is part of the Sphoof framework.
 * Copyright (c) 2010-2011 Sphoof
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code. You can also view the
 * LICENSE file online at http://www.sphoof.nl/new-bsd.txt
 *
 * @category	Sphoof
 * @copyright	Copyright (c) 2010-2011 Sphoof (http://sphoof.nl)
 * @license		http://sphoof.nl/new-bsd.txt	New BSD License
 * @package		Form
 *
 */

require_once realpath( dirname( __FILE__ ) ) . '/exception.php';

class NoSuchAlias extends SpException { }
class SpUnknownTemplate extends SpException { }
class SpDuplicateName extends SpException { }

/**
 * An abstract class all parts of the form must implement. It provides methods
 * for rendering the part.
 */
abstract class SpFormPart {
	/**
	 * The template that should be used to render the element.
	 *
	 * @var String
	 */
	protected $template;

	/**
	 * The context in which the element is embedded. Wihtin a context, no
	 * dubplicate names are allowed
	 *
	 * @var SpFormPart
	 */
	protected $context;

	/**
	 * Sets the new template to use for rendering.
	 *
	 * @param string $template
	 * @return SpFormPart
	 */
	public function useTemplate( $template ) {
		$this->template = $template;
		return $this;
	}

	/**
	 * Returns the templatefile for this element. Throws an exception if it
	 * wasn't set.
	 *
	 * @return string
	 */
	protected function template( ) {
		if( null === $this->template ) { // bad things would happen.
			throw new SpUnknownTemplate( sprintf( 'No template for element "%s"', get_class( $this ) ) );
		}
		return $this->template;
	}

	abstract public function render( );

	/**
	 * Pick the usable values from an Array
	 * Any Array passed to this method should return an associative Array
	 * with one key-value-pair for each element in names().
	 *
	 * @param Array $submitted
	 * @return Array
	 */
	abstract public function values( $submitted );

	/**
	 * Retrieve all names used in the form
	 *
	 * @return Array
	 */
	abstract public function names();

	/**
	 * Not every element needs a context. When changing contexts, we should
	 * check for duplicate names. This is an operation with little practical
	 * use and a large overhead.
	 *
	 * @param SpFormpart $context
	 */
	public function setContext( SpFormpart $context ) {
		$this->context = $context;
	}

	public function root( ) {
		return isset($this->context)
			? $this->context->root( )
			: $this;
	}

}

/**
 * An abstract class that can initiate and hold a collection of elements.
 */
abstract class SpElementCollection extends SpFormPart {
	/**
	 * An array of all elements in this collection.
	 * @var Array
	 */
	protected $elements = array();

	/**
	 * An array of alias to classname mappings.
	 * @var Array
	 */
	protected $aliases = array(
		'text' => 'SpTextElement',
		'password' => 'SpPasswordElement',
		'submit' => 'SpSubmitElement',
		'reset' => 'SpResetElement',
		'hidden' => 'SpHiddenElement',
		'checkbox' => 'SpCheckboxElement',
		'textarea' => 'SpTextareaElement',
		'select' => 'SpSelectElement',
		'radio' => 'SpRadioListElement',
		'fieldset' => 'SpFieldset'
	);

	/**
	 *
	 * @param <type> $method
	 * @param <type> $args
	 * @return <type>
	 */
	public function __call( $method, $args ) {
		return $this->add(
			$this->create(
				$this->classname( $method ),
				$this->arguments( $method, $args )
			)
		);
	}

	public function add( SpFormPart $element ) {
		$overlap = array_intersect( $element->root( )->names( ), $this->root( )->names( ) );
		if ( count($overlap) > 0 ) {
			throw new SpDuplicateName( 'Duplicate names: '.implode(', ',$overlap) );
		}
		$this->elements[] = $element;
		$element->setContext( $this );
		return $element;
	}

	public function elements( ) {
		return (array) $this->elements;
	}

	protected function classname( $alias ) {
		if( !isset( $this->aliases[$alias] ) ) {
			throw new NoSuchAlias( sprintf( 'There is no classname with the alias "%s"', $alias ) );
		}
		return $this->aliases[$alias];
	}

	// @todo	Why is the $method requested here?
	protected function arguments( $method, $args ) {
		return array(
			'name' => array_shift( $args ),
			'label' => array_shift( $args ),
			'value' => array_shift( $args ),
			'id' => array_shift( $args ),
			'attributes' => array_shift( $args )
		);
	}

	protected function create( $classname, $arguments ) {
		return new $classname( $arguments );
	}

	/**
	 * @param Array $submitted
	 * @return Array
	 */
	public function values( $submitted ) {
		$values = Array( );
		foreach( $this->elements as $element ) {
			$values += $element->values( $submitted );
		}
		return $values;
	}

	/**
	 * Return all names in use to be able to check for duplicates
	 *
	 * @return Array
	 */
	public function names( ) {
		$names = Array( );
		foreach( $this->elements as $element ) {
			$names = array_merge( $names,$element->names( ) );
		}
		return $names;
	}

}

/**
 * Representation of the form.
 */
class SpForm extends SpElementCollection {
	/**
	 * The path to look for the templates.
	 *
	 * @var string
	 */
	protected $path;

	/**
	 * The template that renders the form.
	 * @var String
	 */
	protected $template = 'form.html';

	/**
	 * Contains an instance of SpPhpRenderer so the form can render itself.
	 *
	 * @var SpPhpRenderer
	 */
	protected $renderer;

	/**
	 * Constructs the form.
	 *
	 * @param string $action
	 * @param string $method
	 */
	public function __construct( $action = '', $method = 'post' ) {
		$this->action = $action;
		$this->method = $method;
		$this->path = realpath( dirname( __FILE__ ) ) . '/form/templates/';
	}

	/**
	 * Returns the forms action.
	 *
	 * @return String
	 */
	public function action( ) {
		return $this->action;
	}

	/**
	 * Returns the forms method.
	 *
	 * @return string
	 */
	public function method( ) {
		return $this->method;
	}

	/**
	 * A method to overwrite the path to look in for the form's templates.
	 *
	 * @param string $path
	 * @return SpForm
	 */
	public function path( $path ) {
		$this->path = $path;
		return $this;
	}

	public function render( ) {
		// @todo: voeg alleen pad-verwijzing toe als de template naam relatief is?
		foreach( $this->elements as $element ) {
			$element->view( $this->renderer( ) );
		}
		return $this->renderer( )->path( $this->path )->render( $this->template( ), array( 'form' => $this ) );
	}

	public function __toString( ) {
		return (string) $this->render( );
	}

	public function useTemplate( $template ) {
		$this->template = $template;
		$this->path = null;
		return $this;
	}

	/**
	 * Returns an instance of the renderer, and creates it if it didn't exist yet.
	 *
	 * @return SpPhpRenderer
	 */
	protected function renderer( ) {
		if( null === $this->renderer ) {
			$this->renderer = new SpPhpRenderer( );
		}
		return $this->renderer;
	}

}

class SpFieldset extends SpElementCollection {
	/**
	 * The template that renders the fieldset.
	 * @var String
	 */
	protected $template = 'fieldset.html';

	protected $legend;

	protected $view;

	public function  __construct( $info ) {
		$this->legend = $info['name'];
	}

	public function view( SpViewRenderer $view ) {
		$this->view = $view;
		return $this;
	}

	public function render( ) {
		foreach( $this->elements as $element ) {
			$element->view( $this->view );
		}
		return $this->view->render( $this->template( ), array( 'fieldset' => $this ) );
	}

	public function legend( ) {
		return $this->legend;
	}

}

/**
 * An abstract class all form elements should implement.
 */
abstract class SpFormElement extends SpFormPart {
	protected $id;
	protected $name;
	// @todo Every formpart should have the option to use attributes
	protected $attributes = array( );
	protected $value;
	protected $template;
	protected $view;

	/**
	 * Construct the SpFormElement.
	 *
	 * @param string $name
	 * @param string $id
	 * @param mixed $value
	 * @param array $attributes
	 */
	public function __construct( Array $arguments ) {
		$this->extract( $arguments );
		if( null === $this->name ) {
			throw new SpInvalidArgument( sprintf( '%s expects at least one parameter.', get_class( $this ) ) );
		}
	}

	protected function extract( $values ) {
		foreach( $values as $key => $value ) {
			$this->$key = $value;
		}
	}

	public function type( ) {
		return $this->type;
	}

	public function id( ) {
		return isset( $this->id ) ? $this->id : $this->name;
	}

	public function name( ) {
		return $this->name;
	}

	public function label( ) {
		return isset( $this->label ) ? $this->label : ucfirst( str_replace( '_', ' ', $this->name ) );
	}

	public function value( ) {
		return $this->value;
	}

	public function attributes( ) {
		foreach( (array) $this->attributes as $attribute => $value ) {
			$parsed[] = sprintf( '%s="%s"', $attribute, $value );
		}
		return isset( $parsed ) ? ( implode( ' ', $parsed ) . ' ' ) : null;
	}

	/**
	 * Set the view that will be used to render this element.
	 *
	 * @param SpView $view
	 * @return SpFormElement
	 */
	public function view( SpViewRenderer $view ) {
		$this->view = $view;
		return $this;
	}

	public function render( ) {
		return $this->view->render( $this->template( ), array( 'element' => $this ) );
	}

	/**
	 * Return the used names in this formpart. Because it is only one element,
	 * the Array will contain only its one name.
	 *
	 * @return Array
	 */
	public function names( ) {
		return Array( $this->name );
	}

}

class SpTextElement extends SpFormElement {
	protected $template = 'text.html';
	protected $type = 'text';

	/**
	 * @param Array $submitted
	 * @return Array
	 */
	public function values( $submitted ) {
		if ( isset($submitted[$this->name]) ) {
			$this->value = $submitted[$this->name];
		}
		return Array( $this->name => $this->value );
	}

}

class SpHiddenElement extends SpFormElement {
	protected $template = 'hidden.html';
	protected $type = 'hidden';

	/**
	 * @param Array $submitted
	 * @return Array
	 */
	public function values( $submitted ) {
		if ( isset($submitted[$this->name]) ) {
			$this->value = $submitted[$this->name];
		}
		return Array( $this->name => $this->value );
	}

}

class SpPasswordElement extends SpFormElement {
	protected $template = 'password.html';
	protected $type = 'password';

	/**
	 * @param Array $submitted
	 * @return Array
	 */
	public function values( $submitted ) {
		if ( isset($submitted[$this->name]) ) {
			return Array( $this->name => $submitted[$this->name] );
		}
		return Array( $this->name => '' );
	}

}

class SpSubmitElement extends SpFormElement {
	protected $template = 'submit.html';
	protected $type = 'submit';

	/**
	 * Be careful when using this to determine wether a form was submitted, since not all
	 * browsers always send along the submit-button
	 *
	 * @param Array $submitted
	 * @return Array The value is a boolean telling if this button was send along or not
	 */
	public function values( $submitted ) {
		return array( $this->name => isset( $submitted[$this->name] ) );
	}
}

class SpResetElement extends SpFormElement {
	protected $template = 'reset.html';
	protected $type = 'reset';

	public function values( $submitted ) {
		return array();
	}
}

class SpCheckboxElement extends SpFormElement {
	protected $template = 'checkbox.html';
	protected $type = 'checkbox';

	public function values( $submitted ) {
		$this->value = isset( $submitted[$this->name] ) and $submitted[$this->name];
		return array( $this->name => $this->value );
	}

}

class SpTextareaElement extends SpFormElement {
	protected $template = 'textarea.html';
	protected $type = 'textarea';

	/**
	 * @param Array $submitted
	 * @return Array
	 */
	public function values( $submitted ) {
		if ( isset($submitted[$this->name]) ) {
			$this->value = $submitted[$this->name];
		}
		return Array( $this->name => $this->value );
	}

}

class SpSelectElement extends SpFormElement {
	protected $template = 'select.html';
	protected $type = 'select';
	protected $options;

	/**
	 * If $this->options is an associative Array, use the keys as option values,
	 * and the values as description. If the keys are simply numbers in
	 * ascending order, the option values are identical to the description and
	 * could be omitted in the rendering
	 *
	 * If $this->value is not in the Array, it's added to the Array as an
	 * invalid option. This could be used for example as an entry that reads
	 * '--- Choose One ---'
	 *
	 * @param array $arguments
	 */
	public function __construct( Array $arguments ) {
		parent::__construct($arguments);
		if ( is_array($this->options) ) {
			$this->setOptions( $this->options );
		}
	}

	/**
	 * Set the options list after construction
	 *
	 * @param array $options
	 */
	public function setOptions( Array $options ) {
		$this->options = $options;
		if ( array_values( $this->options ) === $this->options ) {
			$this->options = array_combine( $this->options, $this->options );
		}
	}

	/**
	 * Retrieve the options list
	 *
	 * @return Array;
	 */
	public function options(  ) {
		return (array)$this->options;
	}

	/**
	 * @param Array $submitted
	 * @return Array
	 */
	public function values( $submitted ) {
		if ( isset($submitted[$this->name]) and isset($this->options[$submitted[$this->name]]) ) {
			$this->value = $submitted[$this->name];
			return Array( $this->name => $this->value );
		}
		if ( isset($this->value) and isset($this->options[$this->value]) ) {
			return Array( $this->name => $this->value );
		}
		return Array( $this->name => '' );
	}

}

class SpRadiolistElement extends SpFormElement {
	protected $template = 'radiolist.html';
	protected $options;
	protected $type = 'radiolist';

	/**
	 * If $this->options is an associative Array, use the keys as option values,
	 * and the values as description. If the keys are simply numbers in
	 * ascending order, the option values are identical to the description and
	 * could be omitted in the rendering
	 *
	 * $this->value should be a key in the $this->options Array
	 *
	 * @param array $arguments
	 */
	public function __construct( Array $arguments ) {
		parent::__construct($arguments);
		if ( array_values($this->options) ===  $this->options ) {
			$this->options = array_combine( $this->options, $this->options );
		}
		if ( isset($this->value) and !isset($this->options[$this->value]) ) {
			throw new SpException('Non-existent default option "'.$this->value.'" for "'.$this->name.'".');
		}
	}

	/**
	 * @param Array $submitted
	 * @return Array
	 */
	public function values( $submitted ) {
		if ( isset($submitted[$this->name]) and isset($this->options[$submitted[$this->name]]) ) {
			$this->value = $submitted[$this->name];
			return Array( $this->name => $this->value );
		}
		if ( isset($this->value) and isset($this->options[$this->value]) ) {
			return Array( $this->name => $this->value );
		}
		return Array( $this->name => '' );
	}

}